/*
 * *
 *  *  Serponix is an arcade game in focus to multiplayer based on the classic game Snake.
 *  *  Copyright (C) 2010 - 2011  Daniel Vala
 *  *
 *  *  This program is free software: you can redistribute it and/or modify
 *  *  it under the terms of the GNU General Public License as published by
 *  *  the Free Software Foundation, either version 3 of the License,
 *  *  or  any later version.
 *  *
 *  *  This program is distributed in the hope that it will be useful,
 *  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  *  GNU General Public License for more details.
 *
 *  *  You should have received a copy of the GNU General Public License
 *  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *  *
 *  *  If you have any question, do not hesitate to contact author
 *  *  on e-mail address: danielvala42@gmail.com
 *
 */
package com.serponix.settings;

import com.serponix.game.Consts;
import com.serponix.game.GameModeEnum;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.swing.JOptionPane;

/**
 * Provides all settings in the game. Can be saved to configuration file or
 * loaded from it. Can be also sets to default values.
 *
 * @author Daniel Vala
 */
public class GameSettings {

	public static final int NUMBER_OF_HUMAN_PLAYERS = 4;
	private static final SnakeAction[] KEYS = { SnakeAction.UP, SnakeAction.DOWN, SnakeAction.LEFT, SnakeAction.RIGHT, SnakeAction.FIRE, SnakeAction.FIRE2 };
	private static final String NAME_OF_FILE = "settings.txt";
	// names of game setting variables in configuration file
	private static final String SETTING_SPEED_NAME = "speed";
	private static final String SETTING_SERVER_IP_NAME = "serverIP";
	private static final String SETTING_NUMBER_OF_ACTIVE_PLAYERS_NAME = "numberOfActivePlayers";
	private static final String SETTING_PORT_NAME = "port";
	private static final String SETTING_GAME_MODE_NAME = "gameMode";
	private static final String SETTING_PLAYER_NAME = "player";
	private static final String SETTING_KEYS_NAME = "keys";
	private static GameSettings instance;
	// current game settings
	private int speed;
	private String serverIP;
	private int numberOfActivePlayers;
	private int port;
	private GameModeEnum mode;
	private String[] playerNames;
	private List<Map<SnakeAction, Integer>> allKeysMap;

	/**
	 * Singleton to be created only once.
	 */
	private GameSettings() {
		playerNames = new String[NUMBER_OF_HUMAN_PLAYERS];
		allKeysMap = new ArrayList<Map<SnakeAction, Integer>>();
		for (int i = 0; i < NUMBER_OF_HUMAN_PLAYERS; i++) {
			allKeysMap.add(new EnumMap<SnakeAction, Integer>(SnakeAction.class));
		}
		loadFromFileOrSetDefault();
	}

	/**
	 * Get the singleton instance of GameSettings. If does not exist yet, it is
	 * created.
	 *
	 * @return Instance of the only one GameSettings object.
	 */
	public static GameSettings getInstance() {
		if (instance == null) {
			instance = new GameSettings();
		}
		return instance;
	}

	/**
	 * Load configuration from file to the memory. If the configuration file
	 * does not exists yet, default configuration is set to memory and also
	 * saved to newly created file.
	 */
	private void loadFromFileOrSetDefault() {
		if (new File(Consts.GAME_DIR + NAME_OF_FILE).exists()) {
			load();
		} else {
			setDefault();
			save();
		}
	}

	/**
	 * Loads configuration from file to the memory. Configuration file has to be
	 * available in the game directory. If some property or its value is missing
	 * or is in a bad format, the default value of the bad property is set.
	 */
	public void load() {
		boolean saveNeeded = false;
		FileInputStream input = null;
		try {
			input = new FileInputStream(new File(Consts.GAME_DIR + NAME_OF_FILE));
		} catch (FileNotFoundException ex) {
			// TODO log
		}
		Properties prop = new Properties();
		try {
			prop.load(input);
		} catch (IOException ex) {
			// TODO log
		} finally {
			try {
				if (input != null) {
					input.close();
				}
			} catch (IOException ex) {
				JOptionPane.showMessageDialog(null, "Chyba při zavírání souboru nastavení.", "Chyba", JOptionPane.ERROR_MESSAGE);
			}
		}

		try {
			setSpeed(Integer.parseInt(prop.getProperty(SETTING_SPEED_NAME)));
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setSpeed(DefaultSettingsConsts.DEFAULT_SPEED);
			saveNeeded = true;
		}

		try {
			setServerIP(prop.getProperty(SETTING_SERVER_IP_NAME));
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setServerIP(DefaultSettingsConsts.DEFAULT_SERVER_IP);
			saveNeeded = true;
		}

		try {
			setNumberOfActivePlayers(Integer.parseInt(prop.getProperty(SETTING_NUMBER_OF_ACTIVE_PLAYERS_NAME)));
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setNumberOfActivePlayers(DefaultSettingsConsts.DEFAULT_NUMBER_OF_ACTIVE_PLAYERS);
			saveNeeded = true;
		}

		try {
			setPort(Integer.parseInt(prop.getProperty(SETTING_PORT_NAME)));
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setPort(DefaultSettingsConsts.DEFAULT_PORT);
			saveNeeded = true;
		}

		try {
			setGameMode(GameModeEnum.valueOf(prop.getProperty(SETTING_GAME_MODE_NAME)));
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setGameMode(DefaultSettingsConsts.DEFAULT_GAME_MODE);
			saveNeeded = true;
		}

		try {
			for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
				setNameOfGivenPlayer(playerNumber, prop.getProperty(SETTING_PLAYER_NAME + (playerNumber + 1))); // not zero based counting in settings file
			}
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setDefaultNamesOfAllPlayers();
			saveNeeded = true;
		}

		try {
			for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
				// read keys of all player
				String keys = prop.getProperty(SETTING_KEYS_NAME + (playerNumber + 1)); // not zero based counting in settings file
				StringTokenizer st = new StringTokenizer(keys, ",");
				int i = 0;
				while (st.hasMoreTokens()) {
					getAllKeys().get(playerNumber).put(KEYS[i], Integer.parseInt(st.nextToken().trim()));
					i++;
				}
			}
		} catch (Exception ex) {
			// TODO log bad settings of configuration file
			setDefaultKeysOfAllPlayers();
			saveNeeded = true;
		}

		if (saveNeeded) {
			save();
		}
	}

	/**
	 * Returns the configuration file. <br />
	 * If it does not exists, it is created. <br />
	 * If some folders where configuration file should be located do not exist,
	 * they are also created.
	 */
	private File getConfigurationFile() {
		File configurationFile = new File(Consts.GAME_DIR + NAME_OF_FILE);
		File parent = configurationFile.getParentFile();
		if (!parent.exists()) {
			boolean success = parent.mkdirs();
			if (!success) {
				throw new IllegalStateException("Couldn't create dir: " + parent);
			}
		}
		try {
			configurationFile.createNewFile();
		} catch (IOException ex) {
			throw new IllegalStateException("Couldn't create configuration file: " + configurationFile, ex);
		}
		return configurationFile;
	}

	/**
	 * Saves configuration from the memory to the file. If the configuration
	 * file exists, it is overridden. If the configuration file or directories
	 * do not exists, they are created. <br />
	 * This method also writes properties to the configuration file in proper
	 * sequence in contrast with
	 * <code>saveWithBadSequence</code> method, which uses
	 * <code>setProperty</code> method.
	 */
	public void save() {
		PrintWriter writer = null;
		try {
			writer = new PrintWriter(getConfigurationFile(), "UTF-8");
			writer.println("# Serponix settings:");
			writer.println(SETTING_SPEED_NAME + "=" + getSpeed());
			writer.println(SETTING_SERVER_IP_NAME + "=" + getServerIP());
			writer.println(SETTING_NUMBER_OF_ACTIVE_PLAYERS_NAME + "=" + getNumberOfActivePlayers());
			writer.println(SETTING_PORT_NAME + "=" + getPort());
			writer.println(SETTING_GAME_MODE_NAME + "=" + getGameMode().getKey());

			for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
				writer.println((SETTING_PLAYER_NAME + (playerNumber + 1)) + "=" + getNameOfGivenPlayer(playerNumber));
			}

			for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
				Map<SnakeAction, Integer> keysOfOnePlayerMap = getKeysOfGivenPlayer(playerNumber);
				String keysOfOnePlayerString = "";
				for (int keyNumber = 0; keyNumber < KEYS.length; keyNumber++) {
					Integer key = keysOfOnePlayerMap.get(KEYS[keyNumber]);
					if (keyNumber == 0) {
						keysOfOnePlayerString += key;
					} else {
						keysOfOnePlayerString += ", " + key;
					}
				}
				writer.println((SETTING_KEYS_NAME + (playerNumber + 1)) + "=" + keysOfOnePlayerString);
			}
		} catch (FileNotFoundException ex) {
			// TODO log
		} catch (UnsupportedEncodingException ex) {
			// TODO log
		} finally {
			if (writer != null) {
				writer.close();
			}
		}
	}

	/**
	 * Same as
	 * <code>save</code> method, but if the file is not created yet, this method
	 * do not guarantee the sequence of properties written in file. The sequence
	 * does not matter for loading, but it looks better :-)
	 */
	private void saveWithBadSequence() {
		Properties prop = new Properties();
		try {
			prop.setProperty(SETTING_SPEED_NAME, getSpeed() + "");
			prop.setProperty(SETTING_SERVER_IP_NAME, getServerIP());
			prop.setProperty(SETTING_NUMBER_OF_ACTIVE_PLAYERS_NAME, getNumberOfActivePlayers() + "");
			prop.setProperty(SETTING_PORT_NAME, getPort() + "");
			prop.setProperty(SETTING_GAME_MODE_NAME, getGameMode().getKey());

			for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
				prop.setProperty(SETTING_PLAYER_NAME + (playerNumber + 1), getNameOfGivenPlayer(playerNumber));

				Map<SnakeAction, Integer> keysOfOnePlayerMap = getKeysOfGivenPlayer(playerNumber);
				String keysOfOnePlayerString = "";
				for (int keyNumber = 0; keyNumber < KEYS.length; keyNumber++) {
					Integer key = keysOfOnePlayerMap.get(KEYS[keyNumber]);
					if (keyNumber == 0) {
						keysOfOnePlayerString += key;
					} else {
						keysOfOnePlayerString += ", " + key;
					}
				}
				prop.setProperty(SETTING_KEYS_NAME + (playerNumber + 1), keysOfOnePlayerString);
			}
			// save all properties to configuration file
			prop.store(new FileOutputStream(getConfigurationFile()), null);
		} catch (IOException ex) {
			// TODO log
		}
	}

	/**
	 * Sets the current settings in memory to the default values.
	 */
	public void setDefault() {
		setSpeed(DefaultSettingsConsts.DEFAULT_SPEED);
		setServerIP(DefaultSettingsConsts.DEFAULT_SERVER_IP);
		setNumberOfActivePlayers(DefaultSettingsConsts.DEFAULT_NUMBER_OF_ACTIVE_PLAYERS);
		setPort(DefaultSettingsConsts.DEFAULT_PORT);
		setGameMode(DefaultSettingsConsts.DEFAULT_GAME_MODE);
		setDefaultNamesOfAllPlayers();
		setDefaultKeysOfAllPlayers();
	}

	private void setDefaultNamesOfAllPlayers() {
		for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
			setNameOfGivenPlayer(playerNumber, DefaultSettingsConsts.DEFAULT_NAME_OF_PLAYERS[playerNumber]); // not zero based counting in settings file
		}
	}

	private void setDefaultKeysOfAllPlayers() {
		for (int playerNumber = 0; playerNumber < NUMBER_OF_HUMAN_PLAYERS; playerNumber++) {
			for (int keyNumber = 0; keyNumber < KEYS.length; keyNumber++) {
				allKeysMap.get(playerNumber).put(KEYS[keyNumber], DefaultSettingsConsts.DEFAULT_KEYS[playerNumber][keyNumber]);
			}
		}
	}

	/**
	 * Return the predefined speed in game lobby.
	 *
	 * @return The predefined speed.
	 */
	public int getSpeed() {
		return speed;
	}

	/**
	 * Sets speed of the game predefined when user creates a game.
	 *
	 * @param speed Predefined speed.
	 * @throws IllegalArgumentException If speed is negative or too high.
	 */
	public void setSpeed(int speed) throws IllegalArgumentException {
		if (speed >= Consts.MIN_SPEED_OF_THE_GAME && speed <= Consts.MAX_SPEED_OF_THE_GAME) {
			this.speed = speed;
		} else {
			throw new IllegalArgumentException("Speed of the game can be only between " + Consts.MIN_SPEED_OF_THE_GAME + " and " + Consts.MAX_SPEED_OF_THE_GAME);
		}
	}

	/**
	 * Return the predefined server IP when client want to join server.
	 *
	 * @return The predefined server IP.
	 */
	public String getServerIP() {
		return serverIP;
	}

	/**
	 * Sets IP address of the server to be predefined when user want to join a
	 * game.
	 *
	 * @param serverIP Predefined IP address of server.
	 * @throws IllegalArgumentException
	 */
	public void setServerIP(String serverIP) {
		if (serverIP.length() < Consts.MAXIMUM_CHARACTERS_OF_SERVER_IP) {
			this.serverIP = serverIP;
		} else {
			throw new IllegalArgumentException("Server IP can be composed from maximum number of " + Consts.MAXIMUM_CHARACTERS_OF_SERVER_IP + " characters.");
		}
	}

	/**
	 * Return number of active players. By other words, this amount of player is
	 * available to set in the game lobby.
	 *
	 * @return The number of active player.
	 */
	public int getNumberOfActivePlayers() {
		return numberOfActivePlayers;
	}

	/**
	 * Sets the number of players, who can play on one computer.
	 *
	 * @param numberOfActivePlayers The number of active players on one
	 *                              computer.
	 * @throws IllegalArgumentException If the numberOfActivePlayers is not
	 *                                  between 1 and <code>NUMBER_OF_HUMAN_PLAYERS</code>.
	 */
	public void setNumberOfActivePlayers(int numberOfActivePlayers) throws IllegalArgumentException {
		if (numberOfActivePlayers > 0 && numberOfActivePlayers <= NUMBER_OF_HUMAN_PLAYERS) {
			this.numberOfActivePlayers = numberOfActivePlayers;
		} else {
			throw new IllegalArgumentException("Number of active players can be between 0 and " + NUMBER_OF_HUMAN_PLAYERS);
		}
	}

	/**
	 * Return the network port for communication between server and client.
	 *
	 * @return Predefined network port.
	 */
	public int getPort() {
		return port;
	}

	/**
	 * Sets the network port for communication between server and client.
	 *
	 * @param port The network port.
	 * @throws IllegalArgumentException If the port is not in range.
	 */
	public void setPort(int port) {
		if (port >= Consts.MIN_PORT && port <= Consts.MAX_PORT) {
			this.port = port;
		} else {
			throw new IllegalArgumentException("Network port have to be in range from " + Consts.MIN_PORT + " to " + Consts.MAX_PORT);
		}
	}

	/**
	 * Return the predefined game mode in game lobby.
	 *
	 * @return The predefined game mode.
	 */
	public GameModeEnum getGameMode() {
		return mode;
	}

	/**
	 * Sets mode of the game to predefined when user creates a game.
	 *
	 * @param mode The game mode.
	 */
	public void setGameMode(GameModeEnum mode) {
		this.mode = mode;
	}

	/**
	 * Get snake actions and its keys mapped together.
	 *
	 * @param player
	 */
	public Map<SnakeAction, Integer> getKeysOfGivenPlayer(int player) {
		return allKeysMap.get(player);
	}

	public String getNameOfGivenPlayer(int playerNumber) {
		return playerNames[playerNumber];
	}

	/**
	 * Sets name of the given player.
	 *
	 * @param playerNumber
	 * @param name         Name to be set.
	 * @throws IllegalArgumentException If name is empty or too long.
	 */
	public void setNameOfGivenPlayer(int playerNumber, String name) {
		if (name.length() >= Consts.MINIMUM_CHARACTERS_OF_PLAYER_NAME && name.length() < Consts.MAXIMUM_CHARACTERS_OF_PLAYER_NAME) {
			playerNames[playerNumber] = name;
		} else {
			throw new IllegalArgumentException("Name cannot be empty and has to contain maximum " + Consts.MAXIMUM_CHARACTERS_OF_PLAYER_NAME + " characters.");
		}
	}

	public List<Map<SnakeAction, Integer>> getAllKeys() {
		return allKeysMap;
	}

	public void setAllKeys(List<Map<SnakeAction, Integer>> allKeys) {
		this.allKeysMap = allKeys;
	}
}
